package domain;

import gui.GUIController;
import gui.ComponentSwitcher.ComponentChoice;

import java.io.IOException;
import java.util.HashMap;
import java.util.Observable;
import java.util.Observer;
import java.util.Set;
import java.util.Map.Entry;

import javax.swing.JOptionPane;
import server.ServerController;

/**
 * Met de IRCController worden alle kanalen beheert.
 * De klasse is zowel een Observer (server) als een Observable (gui).
 * @author David Covemaeker, Maarten Minnebo, Tim Van Thuyne, Toon Kint
 */

public class IRCController extends Observable implements Observer
{
	private ServerController serverController;
	private String oldLogin = "***";
	private String login;
	private HashMap<String, IRCChannel> channelHashMap;
	private GUIController guiController;
	
	/**
	 * De constructor van de IRCController maakt de channelHashMap aan, die een HashMap is waar alle kanalen in terecht komen.
	 * De HashMap bevat als sleutel een String die de naam van het kanaal bevat en het IRCChannel die aan de naam wordt gelinkt.
	 */
	public IRCController(GUIController guic)
	{
		this.channelHashMap = new HashMap<String, IRCChannel>();
		guiController = guic;
	}
	
	/**
	 * Maakt ServerController aan, die op zijn beurt een ServerReader en -Writer object aanmaakt.
	 * De IRCController wordt observer van de ServerReader.
	 * @param server De gebruikte irc server ("irc.quakenet.org")
	 * @param login Loginnaam van de gebruiker
	 * @param port De gebruikte poort (6667)
	 */
	public boolean connect (String server, String login, int port) throws IOException
	{
		this.login = login;
		serverController = new ServerController(server, login, port);
		
		if(serverController.getSocket() != null)
		{
			serverController.getServerReader().addObserver(this);
			return true;
		}
		else
			return false;
	}
	
	/**
	 * Verbreekt de connectie met de server.
	 * @throws IOException
	 */
	public void disconnect() throws IOException
	{	
		if(!channelHashMap.isEmpty())
		{
			channelHashMap.clear();
			serverController.writeLine("QUIT :Je suis chemin!");
			serverController.closeSocket();
		}
	}
	
	public void changeNick(String newNick) throws IOException
	{
		serverController.writeLine(":" + login + " NICK " + newNick);
		oldLogin = login;
		login = newNick;
	}
	
	/**
	 * Methode update van de interface Observer, wordt opgeroepen als ServerReader tekst ontvangt van de server.
	 * Bepaalt wat voor soort bericht (message) het is en hoe erop gereageerd moet worden.
	 * De reacties zijn a.d.h.v. de Request for Comments 2812. De IRC Protocol.
	 * @param arg0 Het observable object
	 * @param arg1 Het argument dat wordt doorgegeven aan de notifyObservers methode
	 */
	@Override
	public void update(Observable arg0, Object arg1)
	{
		// TODO Auto-generated method stub
		String message = (String) arg1;
		try
		{
			if (
					!pingMessage(message) && !joinMessage(message) && !partMessage(message) && !kickMessage(message) && !nickMessage(message) &&
					!quitMessage(message) && !VersionMessage(message) && !loginError(message) && !userListControl(message) &&
					!MotdMessage(message) && !throttled(message) && message.contains(" PRIVMSG ")
				)
			{
				putLineInChannel(message);
			}
		} 
		catch (IllegalArgumentException iae)
		{
			JOptionPane.showMessageDialog(guiController.getMainFrame().getContentPane(), iae.getMessage(), "Loginnaam niet beschikbaar", JOptionPane.WARNING_MESSAGE);
			if(!(message.contains(oldLogin) && message.contains(login)))
				guiController.getMainFrame().switchToComponent(guiController, ComponentChoice.StartScherm);
		}
		catch (ThrottledException te)
		{
			JOptionPane.showMessageDialog(guiController.getMainFrame().getContentPane(), te.getMessage(), "Server weigert verbinding", JOptionPane.ERROR_MESSAGE);
			guiController.getMainFrame().switchToComponent(guiController, ComponentChoice.StartScherm);
		}
		catch (IOException e)
		{
			JOptionPane.showMessageDialog(guiController.getMainFrame().getContentPane(), "De verbinding is gestopt.", "Verbindingsfout", JOptionPane.ERROR_MESSAGE);
			System.exit(1);
		}
	}
	
	/**
	 * Maakt in alle controles gebruik van:
	 * EXCEL "!"
	 * COLON ":"
	 * POUND "#"
	 */
	
	/**
	 * Plaats een gewone geschreven zin in de channel.
	 * @param line De ingegeven lijn
	 */
	private void putLineInChannel(String line)
	{
		// TODO Auto-generated method stub
		int indexOfExcl = line.indexOf("!");
		String nickname = line.substring(1, indexOfExcl);
		line = line.substring(1);
		int indexOfColon = line.indexOf(":");
		int indexOfPound = line.indexOf("#");
		String channel;
		
		try 
		{
			channel = line.substring(indexOfPound, indexOfColon-1);
		}
		catch(StringIndexOutOfBoundsException e)
		{
			channel = nickname;
		}
		
		String message = line.substring(indexOfColon+1);
		
		try 
		{
			channelHashMap.get(channel.toLowerCase()).addChatMessage(nickname, message);
			
		}
		catch(NullPointerException e)
		{
			channelHashMap.put(channel.toLowerCase(), new IRCChannel(login));
			setChanged();
			notifyObservers(nickname);
			channelHashMap.get(channel.toLowerCase()).addChatMessage(nickname, message);
		}
	}
	
	/**
	 * Schrijft een chatbericht naar het meegegeven kanaal.
	 * @param channel Kanaal waarin bericht is ingegeven
	 * @param line Het ingegeven bericht
	 */
	public void writeToChannel(String channel, String line) throws IOException
	{
		serverController.sayLine(channel, line);
		
		try
		{
			channelHashMap.get(channel.toLowerCase()).addChatMessage(login, line);
		}
		catch (NullPointerException e){}
	}
	
	/**
	 * @param channel Gewenst kanaalnaam.
	 * @return IRCChannel die gelinkt is aan de naam die de paramater channel bevat
	 */
	public IRCChannel getIRCChannel(String channel)
	{
		return channelHashMap.get(channel.toLowerCase());
	}
	
	/**
	 * Client aan kanaal toevoegen.
	 * Nieuw object wordt toevoegt aan de hashmap.
	 * Schrijft een speciaal bericht (JOIN) naar de server.
	 * @param channel Gewenste kanaal
	 * @throws Exception Als client al in het kanaal zit
	 */
	public void joinChannel(String channel) throws Exception
	{
		if (!channelHashMap.containsKey(channel.toLowerCase()))
		{
			serverController.writeLine("JOIN " + channel);
			channelHashMap.put(channel.toLowerCase(), new IRCChannel(login));
		}
		else
		{
			throw new Exception("Client zit al in kanaal.");
		}
	}
	
	/**
	 * Verlaat kanaal.
	 * Object wordt uit hashmap verwijdert.
	 * Schrijft een speciaal bericht (PART) naar de server.
	 * @param channel Kanaal dat client wil verlaten.
	 */
	public void leaveChannel(String channel) throws IOException
	{
		if (channelHashMap.containsKey(channel.toLowerCase()))
		{
			channelHashMap.remove(channel.toLowerCase());
			
			if (channel.startsWith("#"))
				serverController.writeLine("PART " + channel);
		}
	}
	
	/**
	 * Een privaat kanaal wordt geopent tussen twee mensen.
	 * @param nickname De naam van de client waarmee je een privaat gesprek voert
	 * @throws Exception Als je al een privaat kanaal open hebt staan met die persoon
	 */
	public void startPrivateChat(String nickname) throws Exception
	{
		if (!channelHashMap.containsKey(nickname.toLowerCase()))
		{
			channelHashMap.put(nickname.toLowerCase(), new IRCChannel(nickname));
		}
		else
		{
			throw new Exception("Je bent alreeds in private chat met die persoon!");
		}
	}	
	
	/**
	 * Command: PING (3.7.2.) en PONG (3.7.3.)
	 * Nagaan of de verzonden lijn een PING bericht is. Indien dit het geval is moet er gereageerd worden met een PONG bericht.
	 * In de lijn wordt PING dan vervangen door PONG.
	 * @param line De ontvangen lijn
	 * @return match True als de lijn een PING bericht was, anders false
	 */
	private boolean pingMessage(String line) throws IOException
	{
		boolean match = false;
		if (line.toUpperCase().startsWith("PING"))
		{
			serverController.writeLine("PONG" + line.substring(4));
			match = true;
		}
		return match;
	}

	/**
	 * Command: JOIN (3.2.1.)
	 * Kijken of de verzonden lijn een JOIN bericht is.
	 * Het JOIN bericht van JAVA1 op kanaal #HoGent is als volgt:
	 * :JAVA1!jto@tolsun.oulu.fi JOIN #HoGent
	 * @param line De ontvangen lijn
	 * @return match True als boodschap een JOIN is, anders false
	 */
	private boolean joinMessage(String line) 
	{
		boolean match = false;
		if (line.contains(" JOIN ") && !line.contains("PRIVMSG"))
		{
			int indexOfExcl = line.indexOf("!");
			String nickname = line.substring(1, indexOfExcl);
			int indexOfPound = line.indexOf("#");
			String channel = line.substring(indexOfPound);

			channelHashMap.get(channel.toLowerCase()).addClientJoinedChannelMessage(nickname);
			channelHashMap.get(channel.toLowerCase()).addUserToListModel(nickname);
			match = true;
		}
		return match;
	}

	/**
	 * Command: PART (3.2.2.)
	 * Kijken of de verzonden lijn een PART bericht is.
	 * Het PART bericht van JAVA1 die kanaal #HoGent verlaat met bericht "Ik ben weg.", is als volgt:
	 * :JAVA1!jto@tolsun.oulu.fi PART #HoGent :Ik ben weg.
	 * --> controleren op dubbelpunt (:), als die er staat geeft user een reden van vertrek.
	 * @param line De ontvangen lijn
	 * @return match True als boodschap een PART is, anders false
	 */
	private boolean partMessage(String line) 
	{
		boolean match = false;
		if (line.contains(" PART ") && !line.contains("PRIVMSG") && !line.contains(login)) 
		{
			int indexOfExcl = line.indexOf("!");
			String nickname = line.substring(1, indexOfExcl);
			String channel = "";
			String reason = "";
			int indexOfPound = line.indexOf("#");
			line = line.substring(1);
			
			if (line.contains(":")) 
			{
				int indexOfColon = line.indexOf(":");
				channel = line.substring(indexOfPound-1, indexOfColon-1);
				reason = line.substring(indexOfColon+1);
			}
			else 
			{
				channel = line.substring(indexOfPound-1);
			}
			
			System.out.println("|" + channel + "|" + nickname + "|" + reason + "|");
			channelHashMap.get(channel.toLowerCase()).addClientPartChannelMessage(nickname, reason);
			channelHashMap.get(channel.toLowerCase()).removeUserFromListModel(nickname);
			match = true;
		}
		return match;
	}

	/**
	 * Command: KICK (3.2.8.)
	 * Kijken of de verzonden lijn een KICK bericht is.
	 * KICK command zorgt ervoor dat iemand gedwongen uit het kanaal wordt verwijdert.
	 * Het KICK bericht van JAVA1 op kanaal #HoGent om COBOL te verwijderen uit het kanaal #HoGent, is als volgt:
	 * :JAVA1!jto@tolsun.oulu.fi KICK #HoGent COBOL :De reden van vertrek.
	 * --> controleren op dubbelpunt (:), als die er staat is er een reden voor de kick.
	 * @param line De ontvangen lijn
	 * @return match True als boodschap een KICK is, anders false
	 */
	private boolean kickMessage(String line) 
	{
		boolean match = false;
		if (line.contains(" KICK ") && !line.contains("PRIVMSG")) 
		{
			String channel = "";
			String reason = "";
			String channelVictim = "";
			int indexOfSpace;
			
			line = line.substring(1);
			int indexOfExcl = line.indexOf("!");
			String kicker = line.substring(0, indexOfExcl);
			int indexOfPound = line.indexOf("#");
			
			if (line.contains(":"))
			{
				int indexOfColon = line.indexOf(":");
				reason = line.substring(indexOfColon+1);
				channelVictim = line.substring(indexOfPound, indexOfColon-1);
				indexOfSpace = channelVictim.indexOf(" ");
				channel = channelVictim.substring(0, indexOfSpace);
			}
			else
			{
				channelVictim = line.substring(indexOfPound-1);
				indexOfSpace = channelVictim.indexOf(" ");
				channel = channelVictim.substring(0, indexOfSpace);
			}
			
			String victim = channelVictim.substring(indexOfSpace+1);
			channelHashMap.get(channel.toLowerCase()).addClientKickedOutMessage(kicker, victim, reason);
			channelHashMap.get(channel.toLowerCase()).removeUserFromListModel(victim);
			match = true;
		}
		return match;
	}
	
	/**
	 * Command: NICK (3.1.2.)
	 * Kijken of de verzonden lijn een NICK bericht is.
	 * NICK command zorgt ervoor dat iemand een loginnaam kan krijgen of veranderen.
	 * De HashMap wordt doorlopen om alle kanalen te verwittigen dat de user zijn loginnaam heeft veranderd,
	 * enkel de kanalen waar de user zich bevindt gebruiken deze informatie.
	 * Het NICK bericht van JAVA1 om zijn naam te veranderen in JAVA2, is als volgt:
	 * :JAVA1!jto@tolsun.oulu.fi NICK JAVA2
	 * @param line De ontvangen lijn
	 * @return match True als boodschap een NICK is, anders false
	 */
	private boolean nickMessage(String line)
	{
		boolean match = false;
		
		if (line.contains(" NICK ") && !line.contains("PRIVMSG"))
		{
			line = line.substring(1);
			int indexOfExcl = line.indexOf("!");
			String oldName = line.substring(0, indexOfExcl);
			int indexOfColon = line.indexOf(":");
			String newName = line.substring(indexOfColon+1);
			System.out.println(oldName + " heeft zijn bijnaam veranderd naar " + newName);
			
			Set<Entry<String, IRCChannel>> channelSet = channelHashMap.entrySet();
			for(Entry<String, IRCChannel> entry : channelSet) 
			{
				entry.getValue().addNickChangeMessage(oldName, newName);
				entry.getValue().changeLoginName(oldName, newName);
			}
			match = true;
		}
		return match;
	}

	/**
	 * Command: QUIT (3.1.7.)
	 * Kijken of de verzonden lijn een QUIT bericht is.
	 * QUIT command geeft terug dat de client de chat verlaten heeft.
	 * Daarnaast wordt de hashMap doorlopen en worden alle kanalen verwittigt dat de client weg is.
	 * Enkel de kanalen waarin de client aanwezig was, gaat deze informatie gaan gebruiken.
	 * Het QUIT bericht dat toont dat JAVA1 de chat heeft verlaten met als reden "Ik ben weg.", is als volgt:
	 * :JAVA1!jto@tolsun.oulu.fi KICK :Ik ben weg.
	 * @param line De ontvangen lijn
	 * @return match True als boodschap een QUIT is, anders false
	 */
	private boolean quitMessage(String line) 
	{
		boolean match = false;
		if (line.contains(" QUIT ") && !line.contains("PRIVMSG")) 
		{
			line = line.substring(1);
			int indexOfExcl = line.indexOf("!");
			String nickname = line.substring(0, indexOfExcl);
			int indexOfColon = line.indexOf(":Quit:");
			String reason = line.substring(indexOfColon+7);

			Set<Entry<String, IRCChannel>> channelSet = channelHashMap.entrySet();
			for(Entry<String, IRCChannel> entry : channelSet) 
			{
				entry.getValue().addClientQuitServerMessage(nickname, reason);
				entry.getValue().removeUserFromListModel(nickname);
			}
			match = true;
		}
		return match;
	}

	/**
	 * Command: Version (3.4.3.)
	 * Kijken of de verzonden lijn een VERSION bericht is.
	 * @param line De ontvangen lijn
	 * @return match True als boodschap een VERSION is, anders false
	 */
	private boolean VersionMessage(String line)  throws IOException
	{
		boolean match = false;
		line = line.substring(1);
		final char SOH = 1; // ASCII character 0x01
		if (line.substring(0,10).contains("!") && line.contains(SOH+"VERSION"+SOH)) 
		{
			int indexOfExcl = line.indexOf("!");
			String nick = line.substring(0, indexOfExcl);
			serverController.writeLine("NOTICE " + nick + " :" + SOH +"VERSION HoGent Datacom Client:1.0:Java SE 6.0" + SOH);
			match = true;
		}
		return match;
	}
	
	/**
	 * Gaat na of de loginnaam geldig is (controleert op error replies), anders werpt hij een IllegalArgumentException.
	 * Error replies:
	 * 432 - ERR_ERRONEUSNICKNAME - Nadat een ongeldige nickname is doorgegeven.
	 * 433 -  ERR_NICKNAMEINUSE -  Nadat een bestaande nickname is doorgegeven.
	 * @param line De ontvangen lijn
	 * @return match Toont of de lijn de error reply 432 of 433 was of niet.
	 */
	private boolean loginError(String line) throws IllegalArgumentException
	{
		boolean match = false;
		if ((line.contains("433") || line.contains("432")) && !line.contains("PRIVMSG")) 
		{
			match = true;
			throw new IllegalArgumentException("Je gekozen loginnaam is al in gebruik.\nProbeer opnieuw.");
		}
		return match;
	}
	
	/**
	 * Kijkt of de lijn een userlist is van een channel (volgens error reply 353):
	 * 353 - RPL_NAMREPLY
	 * "( "=" / "*" / "@" ) <channel>
     *  :[ "@" / "+" ] <nick> *( " " [ "@" / "+" ] <nick> )
     * --> "@" secret channels
     * --> "*" private channels
     * --> "=" public channels
	 * @param line De ontvangen lijn
	 * @return match Toont of de lijn de error reply 353 was of niet.
	 */
	private boolean userListControl(String line)
	{
		boolean match = false;
		if (line.contains(" 353 ") && !line.contains("PRIVMSG")) 
		{
			line = line.substring(1);
			int indexOfColon = line.indexOf(":");
			int indexOfPound = line.indexOf("#");
			String channel = line.substring(indexOfPound, indexOfColon-1);
			String logins = line.substring(indexOfColon+1);

			String[] arrayLogin;
			String delimiter = " ";
			arrayLogin = logins.split(delimiter); // Dit deelt de string (met de logins) op in een array.

			channelHashMap.get(channel.toLowerCase()).addUsersToListModel(arrayLogin);
			match = true;
		}
		return match;
	}
	
	/**
	 * Kijkt of de lijn een Motd message is, staat voor Message of the day.
	 * 376 - RPL_ENDOFMOTD
	 * 
	 * @param line De ontvangen lijn
	 * @return match True als de lijn het einde van de MOTD aangeeft, anders false
	 */
	private boolean MotdMessage(String line) {
		boolean match = false;
		if (line.contains(" 376 ") && !line.contains("PRIVMSG")) {
			setChanged();
			notifyObservers();
			match = true;
		}
		return match;
	}
	
	/**
	 * Controleert of we de server niet te vaak proberen aanroepen om te verbinden en dus overbelasten
	 * 
	 * @param line
	 * @return 
	 * @throws ThrottledException als je de server overbelast
	 */
	private boolean throttled(String line) throws ThrottledException
	{
		if (line.equals("ERROR :Your host is trying to (re)connect too fast -- throttled")) 
		{
			throw new ThrottledException("Je probeert te snel na elkaar te verbinden.\nDe server laat dit niet toe.");
		}
		return false;
	}
	
	/**
	 * Exceptie die wordt gesmeten als je te vaak met dezelfde server probeert te verbinden op korte tijd.
	 */
	private class ThrottledException extends Exception  
	{
		private static final long serialVersionUID = 3001089494818023780L;
		public ThrottledException(String reason) 
		{
			super(reason);
		}
	}
}